package com.appspot.yttv.client;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import com.appspot.yttv.client.ChromelessPlayer.PlayerListener;
import com.appspot.yttv.client.ChromelessPlayer.PlayerState;
import com.appspot.yttv.client.model.Station;
import com.appspot.yttv.client.model.Video;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.HasText;

/**
 * Presents {@link Station}s on a given player/UI.
 * 
 * @author Martin Landers
 */
public class Presenter {

	/** The list of stations we know. */
	private final List<Station> stations;
	
	/** The station we're tuned to (index into stations). */
	private int tunedTo;
	
	/** The player controlled by the presenter. */
	private final ChromelessPlayer player;

	/** Label for displaying title */
	private final HasText title;

	/** Label for displaying station */
	private final HasText station;

	/** Panel showing TV static while idle. */
	private final StaticPanel staticPanel;
	
	/** The current player state. */
	private PlayerState lastState;
	
	/** The time since the current state changed in seconds. */
	private int lastStateChange;
	
	/** The time since the stations were last refreshed in seconds. */
	private int lastStationRefresh;
	
	/** Number of seconds we tolerate a video not starting,
	 *  (we'll skip sooner anyway if we receive an error) */
	private int skipThreshold;
	
	/** Initial value of skipThreshold. Reset to this value after a video
	 *  starts successfully. */
	private static final int INITIAL_SKIP_THRESHOLD = 4;
	
	/** After skipping a video that fails to start, increase the threshold
	 *  for the next video by this amount. */
	private static final int INCREMENT_SKIP_THRESHOLD = 2;

	/** If skipThreshold reaches this, um, threshold then change station. */
	private static final int SKIP_THRESHOLD_THRESHOLD = 16;
	
	/** Number of seconds we tolerate a video not playing state */
	private static final int BUFFERING_THRESHOLD = 5;
	
	/** Refresh station playlists every (just under) 6 hours. */
	private static final int STATION_REFRESH_TIME = 20000;

	private boolean highQuality = true;
	
	/** Video currently playing */
	private Video video;

	private Timer playbackTimer = new Timer() {
		@Override
		public void run() {
			updateTitleDisplay();
			checkPlayerState(player.getState());
			++lastStateChange;
			
			++lastStationRefresh;
			if (lastStationRefresh > STATION_REFRESH_TIME) {
				Console.log("Refreshing station playlists.");
				for (Station station : stations) {
					station.refresh();
				}
				lastStationRefresh = 0;
			}
		}
	};

	/**
	 * Creates a presenter without stations.
	 */
	public Presenter(ChromelessPlayer player, HasText title, HasText station, StaticPanel sp) {
		this.player = player;
		this.stations = new ArrayList<Station>();
		this.tunedTo = -1;
		this.title = title;
		this.station = station;
		this.staticPanel = sp;
		this.lastStateChange = 0;
		this.lastStationRefresh = 0;
		this.skipThreshold = INITIAL_SKIP_THRESHOLD;
		this.player.addPlayerListener(new PlayerListener() {
			public void onReady() {
			}
			public void onStateChange(PlayerState state) {
				if (state != lastState) {
					lastStateChange = 0;
					if (state == PlayerState.PLAYING) {
						staticPanel.hide();
					} else if (Presenter.this.player.getVideoBytesTotal() <= 0) {
						// Video hasn't started yet, so let's show static instead of just
						// a black screen with the "buffering" widget.
						staticPanel.show();
					}
					lastState = state;
				}
			}
			public void onError(int error) {
				if (lastStateChange < 1) {
					// Unsafe to process an error now, since it could be left over from
					// the previous video we were trying to play.
					return;
				}
				switch (error) {
				case 100:
					Console.log("Not found, skipping.");
					break;
				case 101:
					Console.log("Not allowed, skipping.");
					break;
				default:
					Console.log("Unknown error, skipping.");
				}
				playNextVideo();
			}
		});
	}

	private void checkPlayerState(PlayerState state) {
		switch (state) {
		case PLAYING:
		case PAUSED:
			// We're happy with these states.
			skipThreshold = INITIAL_SKIP_THRESHOLD;
			break;
		case STOPPED:
			// The video has ended
			Console.log("STOPPED, going to next video.");
			playNextVideo();
			break;
		case LOADING:
			if (player.getVideoBytesTotal() < 0 &&
					lastStateChange > skipThreshold ||
					lastStateChange > BUFFERING_THRESHOLD) {
				// If video has not started to buffer, skip after skipThreshold.
				// Otherwise, skip after BUFFERING_THRESHOLD.
				// Also increase the skipThreshold for the next video.
				skipThreshold += INCREMENT_SKIP_THRESHOLD;
				if (skipThreshold > SKIP_THRESHOLD_THRESHOLD) {
					Console.log("Skipped too many videos, so changing station.");
					skipThreshold = INITIAL_SKIP_THRESHOLD;
					nextStation();
				}
				Console.log("Stuck in LOADING, skipping.  skipThreshold = " +
						skipThreshold);
				playNextVideo();
			}
			break;
		default:
			if (lastStateChange > BUFFERING_THRESHOLD) {
				// Whatever we're doing, we're not playing. Next!
				Console.log("Over BUFFERING_THRESHOLD, skipping.");
				playNextVideo();
			}
			break;
		}
	}
	
	/**
	 * Adds a station to the presenter.
	 * 
	 * @param station The new station.
	 */
	public void addStation(Station station) {
		this.stations.add(station);
	}

	public boolean addAllStations(Collection<? extends Station> c) {
		return stations.addAll(c);
	}

	/**
	 * Switches to the next station.
	 */
	public void nextStation() {
		this.tunedTo++;
		this.tunedTo %= this.stations.size();
		Console.log("next: tunedTo " + this.tunedTo);
		station.setText(this.stations.get(tunedTo).getName());
		playNextVideo();
	}
	
	/**
	 * Skips the current video and goes to the next.
	 */
	public void skipVideo() {
		Console.log("skip.");
		playNextVideo();
	}
	
	/**
	 * Switches to the previous station.
	 */
	public void previousStation() {
		this.tunedTo = (this.tunedTo + this.stations.size() - 1 ) % this.stations.size();
		Console.log("previous: tunedTo " + this.tunedTo);
		station.setText(this.stations.get(tunedTo).getName());
		playNextVideo();
	}
	
	/**
	 * Starts playing videos.
	 */
	public void play() {
		if (this.stations.isEmpty()) {
			throw new IllegalStateException("Need at least one station");
		}
		if (this.tunedTo == -1) {
			this.tunedTo = 0;
		}
		playbackTimer.cancel();
		playbackTimer.scheduleRepeating(1000);
	}
	
	/**
	 * Plays the next video from the station we're tuned to.
	 */
	private void playNextVideo() {
		Station s = this.stations.get(tunedTo);
		// To make sure old title goes away quickly.
		lastStateChange = 0;
		title.setText("");
		video = s.nextVideo();
		if (video != null) {
			Console.log("Loading video " + video.getId());
			player.loadVideoById(video.getId(), 0);
		} else {
			Console.log("video = null, not playing.");
		}
	}
	private void updateTitleDisplay() {
		// Get time within video.
		double time = this.player.getCurrentTime();
	    // adjust for video length
		if (time >= 0 && time < 15 && video != null) {
			title.setText(video.getTitle());
		} else {
			title.setText("");
		}
	}
	
	public void toggleVideoQuality() {
		highQuality = !highQuality;
		String vq = highQuality ? "2" : "1";
		Console.log("Toggling video quality to " + (highQuality ? "high" : "low"));
		player.setFlashVariable("setVideoQuality", vq);
		player.loadVideoById(video.getId(), (int)player.getCurrentTime());
	}
}
